package owg.util.opengl;

import java.awt.Dimension;
import java.awt.Rectangle;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import java.util.Stack;

import com.jogamp.opengl.GL;
import com.jogamp.opengl.GL3;
import com.jogamp.opengl.GLException;

import owg.util.ClassPathStream;
import owg.util.data.KryoInstance;
import owg.util.data.KryoInstance.K;
import owg.util.euclidian.MatrixStack;
import owg.util.opengl.models.HPRF;
import owg.util.opengl.models.VBOF;

public class EGL3 extends EGL<GL3> {
    /**Equivalent of OpenGL1 matrix stack*/
    private final MatrixStack modelview = new MatrixStack(16, "Modelview");
    /**Equivalent of OpenGL1 matrix stack*/
    private final MatrixStack projection = new MatrixStack(16, "Projection");
    /**Equivalent of OpenGL1 matrix stack*/
    private final MatrixStack texture = new MatrixStack(16, "Texture");

    /**GL3 core spec does not have any default rendering capabilities, so we need to define our own shader.*/
    public final ShaderProgram defaultShaderProgram;
    public final ShaderProgram stippleShaderProgram;
    public final ShaderProgram alphaTextureShaderProgram;
    public final ShaderProgram disabledShaderProgram;
    /**White texture to use when texturing is disabled.*/
    private int whiteTexture;

    private final GL3ImmediateModeWrapper immediateMode;

    static class Attribs
    {
	transient EGL3 egl;
	transient GL3 gl;

	final float[] origColor = new float[]{1,1,1,1};
	final float[] preColor = new float[]{1,1,1,1};
	final float[] currentNormal = new float[]{0,0,1};
	final float[] currentTexCoord = new float[]{0,0,0,0};

	/**The currently active texture*/
	private int currentTexture;
	private int currentTextureWidth;
	private int currentTextureHeight;
	void applyTexture()
	{
	    gl.glBindTexture(GL.GL_TEXTURE_2D, currentTexture);
	}

	/**The currently active shader program*/
	private ShaderProgram currentProgram;
	void applyProgram()
	{
	    gl.glUseProgram(currentProgram.program);
	}

	/**The current cull face, used for swapping the cull face mode at runtime.*/
	private int currentCullFace = GL3.GL_BACK;
	private boolean cullFaceEnabled = false;
	void applyCullFace()
	{
	    gl.glCullFace(currentCullFace);
	    if(cullFaceEnabled)
		gl.glEnable(GL.GL_CULL_FACE);
	    else
		gl.glDisable(GL.GL_CULL_FACE);
	}

	/**Remember the viewport setting, as long as it is set with the glViewport function in this class.*/
	public final Rectangle viewport = new Rectangle();
	void applyViewport()
	{
	    gl.glViewport(viewport.x, viewport.y, viewport.width, viewport.height);
	}
	/**Remember the scissor setting, as long as it is set with the glScissor function in this class.*/
	public final Rectangle scissor = new Rectangle();
	public boolean scissorEnabled = false;
	void applyScissor()
	{
	    gl.glScissor(scissor.x, scissor.y, scissor.width, scissor.height);
	    if(scissorEnabled)
		gl.glEnable(GL.GL_SCISSOR_TEST);
	    else
		gl.glDisable(GL.GL_SCISSOR_TEST);
	}
	BlendMode blendMode = BlendMode.NORMAL_PRE;
	public void applyBlendMode() {
	    if(blendMode == BlendMode.REPLACE)
		gl.glDisable(GL.GL_BLEND);
	    else
	    {
		gl.glEnable(GL.GL_BLEND);
		gl.glBlendFunc(blendMode.src, blendMode.dst);
	    }
	}

	void apply()
	{
	    applyTexture();
	    applyProgram();
	    applyCullFace();
	    applyScissor();
	    applyViewport();
	    applyBlendMode();
	}

	@K private Attribs() {}

	Attribs(EGL3 egl)
	{
	    this.egl = egl;
	    this.gl = egl.getGL();
	}

	public Attribs clone()
	{
	    Attribs copy = KryoInstance.kryo.copy(this);
	    copy.egl = this.egl;
	    copy.gl = this.gl;
	    return copy;
	}
    }
    Stack<Attribs> oldAttribs;
    Attribs attribs;

    @Override
    public void setBlendMode(BlendMode bm) 
    {
	attribs.blendMode = bm;
	attribs.applyBlendMode();
    }

    public EGL3(GL3 gl, boolean forcePOTS, boolean vSync) 
    {
	super(gl, forcePOTS, vSync);

	oldAttribs = new Stack<>();
	attribs = new Attribs(this);

	int[] tBuf = new int[1];
	gl.glGenTextures(1, tBuf, 0);
	whiteTexture = tBuf[0];

	immediateMode = new GL3ImmediateModeWrapper(this);

	gl.glBindTexture(GL.GL_TEXTURE_2D, whiteTexture);
	gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_REPEAT);
	gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_REPEAT);
	gl.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST);
	gl.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST);

	ByteBuffer pBuf = asBuffer(new byte[]{(byte)255, (byte)255, (byte)255, (byte)255});
	gl.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_RGBA, 1, 1, 0, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, pBuf);
	attribs.currentTexture = whiteTexture;
	attribs.currentTextureWidth = 1;
	attribs.currentTextureHeight = 1;

	try 
	{
	    final String shaderDir = "owg/util/opengl/shaders/";
	    final String vpath = shaderDir+"default.vert";
	    String fpath;
	    fpath = shaderDir+"default.frag";
	    defaultShaderProgram = ShaderLib.get(this, new ClassPathStream(EGL3.class, vpath), new ClassPathStream(EGL3.class, fpath));
	    fpath = shaderDir+"stipple.frag";
	    stippleShaderProgram = ShaderLib.get(this, new ClassPathStream(EGL3.class, vpath), new ClassPathStream(EGL3.class, fpath));
	    fpath = shaderDir+"alphaTexture.frag";
	    alphaTextureShaderProgram = ShaderLib.get(this, new ClassPathStream(EGL3.class, vpath), new ClassPathStream(EGL3.class, fpath));
	    fpath = shaderDir+"disabled.frag";
	    disabledShaderProgram = ShaderLib.get(this, new ClassPathStream(EGL3.class, vpath), new ClassPathStream(EGL3.class, fpath));
	}
	catch (IOException e) 
	{
	    throw new GLException("Failed to load default shaders... ", e);
	}
	useProgram(defaultShaderProgram);
	attribs.apply();
    }

    @Override
    public GL3 getGL() 
    {
	return gl;
    }
    @Override
    public boolean checkError(String message) 
    {
	if(!gl.getContext().isCurrent()) 
	{
	    System.err.println("Error: Cannot check for errors when context is not current: "+message);
	    new Throwable().printStackTrace();
	    return true;
	}
	else 
	{
	    int error = gl.glGetError();
	    if(error!=GL3.GL_NO_ERROR) 
	    {
		System.err.println(message+": "+glu.gluErrorString(error));
		new Throwable().printStackTrace();
		return true;
	    }
	}
	return false;
    }

    @Override
    protected void stepImpl() 
    {
	checkError("Purge: stepImpl");
    }

    @Override
    public FloatBuffer asBuffer(float[] data) 
    {
	return FloatBuffer.wrap(data);
    }

    @Override
    public IntBuffer asBuffer(int[] data) 
    {
	return IntBuffer.wrap(data);
    }

    @Override
    public ShortBuffer asBuffer(short[] data) 
    {
	return ShortBuffer.wrap(data);
    }

    @Override
    public ByteBuffer asBuffer(byte[] data) 
    {
	return ByteBuffer.wrap(data);
    }

    @Override
    public MatrixStack modelviewMatrix() 
    {
	return modelview;
    }

    @Override
    public MatrixStack textureMatrix() {
	return texture;
    }

    @Override
    public MatrixStack projectionMatrix() {
	return projection;
    }

    public ShaderProgram getCurrentProgram() 
    {
	return attribs.currentProgram;
    }

    @Override
    public HPRF genModel(String name, boolean vDynamicDraw, boolean iDynamicDraw, int faceMode, 
		int nVertexComps, boolean useNormal, int nColorComps, int nTexCoords, int nTexCoords2)
    {
    	return new VBOF(name, vDynamicDraw?GL.GL_DYNAMIC_DRAW:GL.GL_STATIC_DRAW, iDynamicDraw?GL.GL_DYNAMIC_DRAW:GL.GL_STATIC_DRAW,
    		faceMode, nVertexComps, useNormal, nColorComps, nTexCoords, nTexCoords2);
    }

    @Override
    public void enableTexture2D(int texture, int width, int height) 
    {
	attribs.currentTexture = texture;
	attribs.currentTextureWidth = width;
	attribs.currentTextureHeight = height;
	attribs.applyTexture();
    }
    @Override
    public void enableTexture2D(Tex tex, int subImage)
    {
	if(tex == null)
	    disableTexture2D();
	else
	    enableTexture2D(tex.tex(this, subImage), tex.width, tex.height);
    }
    @Override
    public void disableTexture2D() 
    {
	attribs.currentTexture = whiteTexture;
	attribs.currentTextureWidth = 1;
	attribs.currentTextureHeight = 1;
	attribs.applyTexture();
    }

    @Override
    public boolean getPreferVBO()
    {
	return true;
    }

    @Override
    public void setStippleEnabled(boolean b)
    {
	if(b)
	    useProgram(stippleShaderProgram);
	else
	    useProgram(defaultShaderProgram);
    }

    public void useProgram(ShaderProgram program)
    {
	attribs.currentProgram = program;
	attribs.applyProgram();
    }

    @Override
    public void pushAttribs() 
    {
	oldAttribs.push(attribs.clone());
    }

    @Override
    public void popAttribs() 
    {
	attribs = oldAttribs.pop();
	attribs.apply();
    }

    @Override
    public int luminanceFormat() {
	return GL3.GL_RED;
    }

    @Override
    public int alphaFormat() {
	return GL3.GL_RED;
    }
    @Override
    public boolean shouldPremultiplyColor() {
	return attribs.blendMode == BlendMode.NORMAL_PRE;
    }

    @Override
    public ImmediateModeWrapper<GL3> immediateMode() {
	return immediateMode;
    }

    @Override
    public void setViewport(int x, int y, int w, int h)  
    {
	attribs.viewport.setBounds(x, y, w, h);
	attribs.applyViewport();
    }
    @Override
    public Rectangle getViewport() {
        return (Rectangle) attribs.viewport.clone();
    }

    @Override
    public void setScissor(int x, int y, int w, int h) 
    {
	attribs.scissor.setBounds(x, y, w, h);
	attribs.applyScissor();
    }

    @Override
    public void setScissorEnabled(boolean b) 
    {	
	attribs.scissorEnabled = b;
	attribs.applyScissor();
    }

    @Override
    public void setCullFaceEnabled(boolean b) {
	attribs.cullFaceEnabled = b;
	attribs.applyCullFace();
    }

    public void swapCullFace()
    {
	if(attribs.currentCullFace == GL.GL_FRONT)
	    attribs.currentCullFace = GL3.GL_BACK;
	else
	    attribs.currentCullFace = GL3.GL_FRONT;
	attribs.applyCullFace();
    }
    /**Returns the active color. If premultiplied blend mode is available, this will be a premultiplied color.
     * Otherwise, the non-premultiplied color is returned.*/
    public float[] getActiveColor() {
	if(shouldPremultiplyColor())
	    return attribs.preColor;
	else
	    return attribs.origColor;
    }

    @Override
    public void setRGB(ColorF color) {
	setColor(color.getRed(), color.getGreen(), color.getBlue(), attribs.origColor[3]);
    }

    @Override
    public void setAlpha(float alpha) {
	setColor(attribs.origColor[0], attribs.origColor[1], attribs.origColor[2], alpha);
    }
    @Override
    public void setColor(ColorF color) {
	setColor(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha());
    }

    public void setColor(float r, float g, float b, float a) {
	attribs.preColor[0] = r*a;
	attribs.preColor[1] = g*a;
	attribs.preColor[2] = b*a;
	attribs.preColor[3] = a;

	attribs.origColor[0] = r;
	attribs.origColor[1] = g;
	attribs.origColor[2] = b;
	attribs.origColor[3] = a;
    }

    @Override
    public boolean isScissorEnabled()
    {
	return attribs.scissorEnabled;
    }

    @Override
    public Rectangle getScissor()
    {
	return (Rectangle) attribs.scissor.clone();
    }
    public int getCurrentTextureWidth()
    {
	return attribs.currentTextureWidth;
    }
    public int getCurrentTextureHeight()
    {
	return attribs.currentTextureHeight;
    }
    public Dimension getCurrentTextureSize()
    {
	return new Dimension(attribs.currentTextureWidth, attribs.currentTextureHeight);
    }

	@Override
	public void setDisabledFilter(boolean b)
	{
		if(b)
			useProgram(disabledShaderProgram);
		else
			useProgram(defaultShaderProgram);
	}
}
